package org.barad.architecture.analyzer.archpad.component;

import java.awt.Color;
import java.awt.geom.Rectangle2D;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import javax.swing.BorderFactory;
import javax.swing.JTree;
import javax.swing.tree.DefaultTreeModel;

import org.barad.architecture.analyzer.archpad.ArchPad;
import org.barad.architecture.analyzer.archpad.region.Region;
import org.barad.architecture.analyzer.widgets.CustomMutableTreeNode;
import org.jgraph.JGraph;
import org.jgraph.graph.DefaultGraphCell;
import org.jgraph.graph.GraphConstants;
import org.jgraph.graph.ParentMap;

/**
 * This class manages architectural components.
 *  
 * @author svetoslavganov@gmail.com (Svetoslav R. Ganov)
 *
 */
public class ComponentManager {
	private static final String DFS_STATE = "dfs_state";
	private static final String DFS_PARENT = "dfs_parent";
	private static long groupId = 0;
	private ArchPad archPad;

	/**
	 * Creates a new instance.
	 * 
	 * @param archPad The {@link ArchPad} instance.
	 */
	public ComponentManager(ArchPad archPad) {
		this.archPad = archPad;
	}
	
	/**
	 * Groups the selected graph cells into an architectural component.
	 */
	public void groupAsComponent() {
		Object[] cells = archPad.getCustomizedJGraph().getSelectionCells();
		groupAsComponent(cells);
	}
	
	/**
	 * Groups an array of graph cells into an architectural component.
	 * A parent cell is created and all group members become its children.
	 * 
	 * @param cells The cells.
	 * @return The parent cell.
	 */
	public DefaultGraphCell groupAsComponent(Object[] cells) {
		DefaultGraphCell groupParent = null;
		if (cells.length > 0) {
			JGraph jGraph = archPad.getCustomizedJGraph();
			Rectangle2D selectedRectangle = jGraph.getCellBounds(cells);
			groupParent = new DefaultGraphCell();
			ArchPadComponent archPadComponent = new ArchPadComponent();
			archPadComponent.setName("Custom " + groupId++);
			addSubsumedRegionsToComponent(cells, archPadComponent);
			groupParent.setUserObject(archPadComponent);			
			GraphConstants.setBounds(groupParent.getAttributes(), new Rectangle2D.Double(selectedRectangle.getCenterX(), selectedRectangle.getCenterY(), 70, 70));
			GraphConstants.setBorder(groupParent.getAttributes(), BorderFactory.createLineBorder(Color.BLACK));
			archPad.getCustomizedJGraph().getGraphLayoutCache().insert(groupParent);
			ParentMap parentMap = new ParentMap(cells, groupParent);
			jGraph.getGraphLayoutCache().edit(null, null, parentMap, null);
			jGraph.getGraphLayoutCache().collapse(new Object[]{groupParent});
		}
		return groupParent;
	}
	
	/**
	 * Adds all regions from a component's descendants to the component.
	 * 
	 * @param cells The children graph cell of the cell that contains the component.
	 * @param archPadComponent The component.
	 */
	private void addSubsumedRegionsToComponent(Object[] cells, ArchPadComponent archPadComponent) {
		LinkedHashSet<Region> regions = new LinkedHashSet<Region>();
		Object[] descendents = archPad.getCustomizedJGraph().getDescendants(cells);
		for (Object descendent: descendents) {
			if (descendent != archPadComponent) {
				DefaultGraphCell cell = (DefaultGraphCell)descendent;
				ArchPadComponent cmp = (ArchPadComponent)cell.getUserObject();
				if (cmp != null) {
					regions.addAll(cmp.getRegions());
				}
			}
		}
		archPadComponent.getRegions().addAll(regions);
	}
	
	/**
	 * Ungroup all selected components.
	 */
	public void ungroupSelectedComponents() {	
		Object[] selectedCells = archPad.getCustomizedJGraph().getSelectionCells();
		for (Object obj: selectedCells) {
			DefaultGraphCell parent = (DefaultGraphCell)obj;
			ungroupComponent(parent);
		}
	}
	
	/**
	 * Ungroups a component given the parent {@link DefaultGraphCell}.
	 * 
	 * @param parent The parent {@link DefaultGraphCell}.
	 */
	public void ungroupComponent(DefaultGraphCell parent) {
		Object[] children = parent.getChildren().toArray();
		ParentMap parentMap = new ParentMap(children, null);
		archPad.getCustomizedJGraph().getGraphLayoutCache().edit(null, null, parentMap, null);
	}

	/**
	 * Expands a component.
	 */
	public void expandComponent() {
		Object[] groups = getGroups();
		archPad.getCustomizedJGraph().getGraphLayoutCache().expand(groups);
		archPad.getCustomizedJGraph().setEnabledActions();
	}
	
	/**
	 * Collapses a component.
	 */
	public void collapseComponent() {
		Object[] groups = getGroups();
		for (Object group: groups) {
			DefaultGraphCell cell = (DefaultGraphCell)group;
			ArchPadComponent archPadComponent = (ArchPadComponent)cell.getUserObject();
			Object[] descendants = archPad.getCustomizedJGraph().getDescendants(new Object[]{group});
			addSubsumedRegionsToComponent(descendants, archPadComponent);
		}
		archPad.getCustomizedJGraph().getGraphLayoutCache().collapse(groups);
		archPad.getCustomizedJGraph().setEnabledActions();
	}
	
	/**
	 * Gets the cell groups in the current selection. A groups is a cell
	 * that has children.
	 * 
	 * @return The groups.
	 */
	public Object[] getGroups() {
		LinkedList<Object> groupsList = new LinkedList<Object>();
		Object[] cells = archPad.getCustomizedJGraph().getSelectionCells();
		if (cells != null) {
			for (Object obj: cells) {
				DefaultGraphCell cell = (DefaultGraphCell)obj;
				if (hasChildrenCells(cell)) {
					groupsList.add(cell);
				}
			}
		}
		return groupsList.toArray();
	}
	
	/**
	 * Returns if a graph cell has children cells.
	 * 
	 * @param cell The cell.
	 * @return True if the cell has children, false otherwise.
	 */
	private boolean hasChildrenCells(DefaultGraphCell cell) {
		boolean hasChildrenCells = false;
		for (Object obj: cell.getChildren()) {
			if (obj.getClass().equals(DefaultGraphCell.class)) {
				hasChildrenCells = true;
				break;
			}
		}
		return hasChildrenCells;
	}
	
	/**
	 * Creates an interface of a group.
	 *  
	 * @param cells The group member cells.
	 * @return An architectural component that aggregates the group
	 *         members and exposes the group interfaces as an interface
	 *         of a single component. 
	 */
	@SuppressWarnings("all")
	private ArchPadComponent createGroupInterface(Object[] cells) {
		ArchPadComponent archPadComponent = null;
		archPadComponent.setName("Component " + groupId++);
		//TODO: Here we combine the interfaces of group's components and generate the group super interface
		return archPadComponent;
	}
	
	/**
	 * Builds the component hierarchy by performing a DFS traversal
	 * of the selected cells and adds the hierarchy in a {@link JTree}.
	 * 
	 * @param componentHierarchyTree The component hierarchy {@link JTree}.
	 */
	public void buildComponentHierarchy(JTree componentHierarchyTree) {
		DefaultGraphCell parent = (DefaultGraphCell)archPad.getCustomizedJGraph().getSelectionCell();
		Object[] descendants = archPad.getCustomizedJGraph().getDescendants(new Object[]{parent});
		initilaizeNodesForDFSTraversal(descendants);
		Stack<CustomMutableTreeNode> hierarchicalNodes = new Stack<CustomMutableTreeNode>();
		buildHierarchyByDFSTraversal(parent, hierarchicalNodes);
		resetNodesAfterDFSTraversal(descendants);
		if (hierarchicalNodes.size() > 0) {
			DefaultTreeModel model = (DefaultTreeModel)componentHierarchyTree.getModel();
			model.setRoot(hierarchicalNodes.pop());
			componentHierarchyTree.validate();
		}
	}
	
	/**
	 * Performs an initialization if the the cells to be traversed via DFS.
	 * 
	 * @param nodes The cells (nodes).
	 */
	@SuppressWarnings("unchecked")
	private void initilaizeNodesForDFSTraversal(Object[] nodes) {
		for (Object obj: nodes) {
			if (obj != null) {
				DefaultGraphCell cell = (DefaultGraphCell)obj;
				cell.getAttributes().put(ComponentManager.DFS_PARENT, cell);
				cell.getAttributes().put(ComponentManager.DFS_STATE, State.UNKNOWN);
			}
		}
	}
	
	/**
	 * Rests the cells that have been traversed via DFS.
	 * 
	 * @param nodes The cells (nodes).
	 */
	private void resetNodesAfterDFSTraversal(Object[] nodes) {
		for (Object obj: nodes) {
			if (obj != null) {
				DefaultGraphCell cell = (DefaultGraphCell)obj;
				cell.getAttributes().remove(ComponentManager.DFS_PARENT);
				cell.getAttributes().remove(ComponentManager.DFS_STATE);
			}
		}
	}
	
	/**
	 * Creates a new {@link CustomMutableTreeNode}.
	 * 
	 * @param cell The graph cell that contains an ArchPad component
	 *        to be stored in the tree node.
	 * @return The {@link CustomMutableTreeNode} instance.
	 */
	private CustomMutableTreeNode createNode(DefaultGraphCell cell) {
		ArchPadComponent archPadComponent = (ArchPadComponent)cell.getUserObject();
		return new CustomMutableTreeNode(archPadComponent.getTemplateFileName(), archPadComponent);
	}
	
	/**
	 * Builds the component hierarchy performing  DFS traversal and maintaining
	 * a stack to which is added each encountered sub component.
	 * 
	 * @param cell The cell whose descendants are traversed.
	 * @param hierarchicalNodes The stack with architectural components.
	 */
	@SuppressWarnings("unchecked")
	private void buildHierarchyByDFSTraversal(DefaultGraphCell cell, 
			Stack<CustomMutableTreeNode> hierarchicalNodes) {
		ArchPadComponent archPadComponent = (ArchPadComponent)cell.getUserObject();
		if (archPadComponent != null) {
			cell.getAttributes().put(ComponentManager.DFS_STATE, State.DISCOVERED);
			if(archPadComponent.getTemplateFileName() != null) {
				CustomMutableTreeNode newNode = createNode(cell);
				//if this is the root
				if (hierarchicalNodes.size() > 0) {
					hierarchicalNodes.peek().add(newNode);
				}
				hierarchicalNodes.push(newNode);
			}
			List<DefaultGraphCell> adjacentCells = cell.getChildren();
			for (DefaultGraphCell dgc: adjacentCells) {
				Map<Object, Object> attributes = dgc.getAttributes();
				if (attributes.get(ComponentManager.DFS_STATE) == State.UNKNOWN) {
					attributes.put(ComponentManager.DFS_PARENT, cell);
					buildHierarchyByDFSTraversal(dgc, hierarchicalNodes);
				}
			}
			//we leave the root to assign it to the tree model
			if (hierarchicalNodes.size() > 1) {
				//remove a tree node if the cell corresponding to it is discovered
				ArchPadComponent treeComponent = (ArchPadComponent)hierarchicalNodes.peek().getUserObject();
				if (archPadComponent.equals(treeComponent)) {
					hierarchicalNodes.pop();
				}
			}
			cell.getAttributes().put(ComponentManager.DFS_STATE, State.VISITED);
		}
	}
	
	/**
	 * A type safe enumeration of the states during DFS.
	 * 
	 * @author svetoslavganov@gmail.com (Svetoslav R. Ganov)
	 *
	 */
	private enum State { 
		DISCOVERED, 
		VISITED,
		UNKNOWN;
	}	
}
